﻿using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace Library.Winform.Hooks {

  /// <summary>
  /// Captures global keyboard events
  /// </summary>
  public class KeyboardHook : GlobalHook {
    //protected readonly EventHandlerList Events;
    private static readonly object EventKeyDown = new object();
    private static readonly object EventKeyUp = new object();
    private static readonly object EventKeyPress = new object();

    #region Events

    public event KeyEventHandler KeyDown {
      add {
        Events.AddHandler(EventKeyDown, value);
      }
      remove {
        Events.RemoveHandler(EventKeyDown, value);
      }
    }
    public event KeyEventHandler KeyUp {
      add {
        Events.AddHandler(EventKeyUp, value);
      }
      remove {
        Events.RemoveHandler(EventKeyUp, value);
      }
    }
    public event KeyPressEventHandler KeyPress {
      add {
        Events.AddHandler(EventKeyPress, value);
      }
      remove {
        Events.RemoveHandler(EventKeyPress, value);
      }
    }

    #endregion

    #region Constructor

    public KeyboardHook() {

      HookType = WhKeyboardLl;
    }

    #endregion

    #region Methods

    protected override int HookCallbackProcedure(int nCode, int wParam, IntPtr lParam) {

      bool handled = false;
      var keyDownHandle = (KeyEventHandler)Events[EventKeyDown];
      var keyUpHandler = (KeyEventHandler)Events[EventKeyUp];
      var keyPressHandler = (KeyPressEventHandler)Events[EventKeyPress];
      if (nCode > -1 && (keyDownHandle != null || keyUpHandler != null || keyPressHandler != null)) {

        var keyboardHookStruct =
            (KeyboardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyboardHookStruct));

        // Is Control being held down?
        bool control = ((GetKeyState(VkLcontrol) & 0x80) != 0) ||
                       ((GetKeyState(VkRcontrol) & 0x80) != 0);

        // Is Shift being held down?
        bool shift = ((GetKeyState(VkLshift) & 0x80) != 0) ||
                     ((GetKeyState(VkRshift) & 0x80) != 0);

        // Is Alt being held down?
        bool alt = ((GetKeyState(VkLalt) & 0x80) != 0) ||
                   ((GetKeyState(VkRalt) & 0x80) != 0);

        // Is CapsLock on?
        bool capslock = (GetKeyState(VkCapital) != 0);

        // Create event using keycode and control/shift/alt values found above
        var e = new KeyEventArgs(
            (Keys)(
                keyboardHookStruct.vkCode |
                (control ? (int)Keys.Control : 0) |
                (shift ? (int)Keys.Shift : 0) |
                (alt ? (int)Keys.Alt : 0)
                ));

        // Handle KeyDown and KeyUp events
        switch (wParam) {

          case WmKeydown:
          case WmSyskeydown:
            if (keyDownHandle != null) {
              keyDownHandle(this, e);
              handled = e.Handled;
            }
            break;
          case WmKeyup:
          case WmSyskeyup:
            if (keyUpHandler != null) {
              keyUpHandler(this, e);
              handled = e.Handled;
            }
            break;

        }

        // Handle KeyPress event
        if (wParam == WmKeydown &&
           !handled &&
           !e.SuppressKeyPress &&
            keyPressHandler != null) {

          var keyState = new byte[256];
          var inBuffer = new byte[2];
          GetKeyboardState(keyState);

          if (ToAscii(keyboardHookStruct.vkCode,
                    keyboardHookStruct.scanCode,
                    keyState,
                    inBuffer,
                    keyboardHookStruct.flags) == 1) {

            var key = (char)inBuffer[0];
            if ((capslock ^ shift) && Char.IsLetter(key))
              key = Char.ToUpper(key);
            var e2 = new KeyPressEventArgs(key);
            keyPressHandler(this, e2);
            handled = e.Handled;

          }

        }

      }

      if (handled) {
        return 1;
      }
      return CallNextHookEx(HandleToHook, nCode, wParam, lParam);
    }

    #endregion

  }

}
